Reducer and Context Patterns in React JS
Reducer and Context Patterns are React state management techniques used to organize complex state logic and share data between components without repeatedly passing props. The useReducer Hook manages state transitions, while the Context API makes state and dispatch functions accessible to components throughout a component tree.
React JS Training: JustAcademy React JS Training Course
Course Demo: Register for React JS Course Demo
1. Learning Objectives
After completing this topic, you will be able to:
- Understand reducers and the Context API in React.
- Use the
useReducer, createContext, and useContext APIs.
- Manage complex state updates using reducer functions.
- Share state across deeply nested components without prop drilling.
- Combine Reducer and Context patterns to create reusable state management logic.
- Build a practical task management application using React.
2. What Is a Reducer in React?
A reducer is a JavaScript function that receives the current state and an action, processes the action, and returns the next state. In React, reducers are commonly used with the useReducer Hook to organize state update logic in one place.
Syntax:
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
return state;
}
}
In this example, the reducer checks the action type and returns a new state object based on the requested operation.
Key Characteristics of a Reducer
- It receives the current state and an action.
- It calculates and returns the next state.
- It should be a pure function.
- It should not directly modify the existing state.
- It should not perform side effects such as API calls or DOM manipulation.
- It can handle multiple types of state updates in one centralized function.
3. What Is the useReducer Hook?
The useReducer Hook is a React Hook used to manage state when updates involve multiple actions or more complex logic. It is an alternative to useState.
Syntax:
const [state, dispatch] = useReducer(reducer, initialState);
Parameters and Return Values
| Element |
Description |
reducer |
A function that calculates the next state from the current state and an action. |
initialState |
The initial value of the state. |
state |
The current state value returned by the Hook. |
dispatch |
A function used to send an action to the reducer. |
Example: Counter Using useReducer
import { useReducer } from "react";
const initialState = { count: 0 };
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error("Unknown action: " + action.type);
}
}
export default function Counter() {
const [state, dispatch] = useReducer(
counterReducer,
initialState
);
return (
<div>
<h2>Count: {state.count}</h2>
<button onClick={() => dispatch({ type: "increment" })}>
Increment
</button>
<button onClick={() => dispatch({ type: "decrement" })}>
Decrement
</button>
<button onClick={() => dispatch({ type: "reset" })}>
Reset
</button>
</div>
);
}
How It Works
- The component initializes state with
useReducer.
- The user clicks a button.
- The component calls
dispatch() with an action object.
- React calls the reducer with the current state and that action.
- The reducer returns the next state.
- React renders the component with the updated state.
4. What Is the Context API?
Context is a React feature that allows a parent component to provide information to components anywhere below it in the component tree without passing that information through every intermediate component as props.
It is useful when many components need access to shared information, such as a theme, authenticated user, language setting, shopping cart, or application state.
Important Context APIs
| API |
Purpose |
createContext() |
Creates a context object. |
| Context Provider |
Makes a value available to descendant components. |
useContext() |
Reads the value from the closest matching provider above the component. |
Basic Context Example
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function ThemeButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Current theme: {theme}</button>;
}
export default function App() {
return (
<ThemeContext value="dark">
<ThemeButton />
</ThemeContext>
);
}
Note: In current React documentation, a context object can be rendered directly as a provider using . In React versions before React 19, use .
5. What Is Prop Drilling?
Prop drilling occurs when data must be passed through multiple intermediate components even though those components do not directly need the data themselves.
Example of Prop Drilling
function App() {
const user = { name: "Aman" };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Profile user={user} />;
}
function Profile({ user }) {
return <h2>Welcome, {user.name}</h2>;
}
Here, Layout and Dashboard only pass the user information along. Context can remove the need for these intermediate props when the data is shared across a deeper component tree.
Benefits of Context
- Reduces repetitive prop passing.
- Makes shared data easier to access.
- Supports cleaner component interfaces.
- Helps organize state shared by multiple components.
6. What Is the Reducer and Context Pattern?
The Reducer and Context pattern combines useReducer with Context. The reducer manages state transitions, and Context distributes the state and dispatch function to components that need them.
It is particularly useful for complex screens where several components must read or update the same state.
How the Pattern Works
- Create a reducer function containing the state update logic.
- Initialize state with
useReducer.
- Create Context objects for the state and dispatch function.
- Provide the values to descendant components.
- Use
useContext in components that need the state or dispatch function.
- Dispatch actions to update state through the reducer.
Architecture Flow
User Interaction
|
v
dispatch(action)
|
v
Reducer(state, action)
|
v
New State
|
v
Context Provider
|
v
Components Read Updated State
7. Building a Reducer and Context Pattern Step by Step
Step 1: Create the Context File
Create a file named TasksContext.jsx. Define separate contexts for task data and the dispatch function.
import { createContext } from "react";
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
Keeping state and dispatch in separate contexts is useful when some components only need to trigger actions and do not need to read the state.
Step 2: Create the Reducer
The reducer handles adding, editing, toggling, and deleting tasks.
export function tasksReducer(tasks, action) {
switch (action.type) {
case "added":
return [
...tasks,
{
id: action.id,
text: action.text,
done: false
}
];
case "changed":
return tasks.map(task =>
task.id === action.task.id ? action.task : task
);
case "deleted":
return tasks.filter(task => task.id !== action.id);
default:
throw new Error("Unknown action: " + action.type);
}
}
Understanding the Actions
| Action Type |
Purpose |
Required Data |
added |
Adds a new task. |
id, text |
changed |
Updates an existing task. |
task |
deleted |
Removes a task. |
id |
Step 3: Create a Provider Component
The provider owns the reducer and supplies both the task state and dispatch function to its children.
import { useReducer } from "react";
import {
TasksContext,
TasksDispatchContext
} from "./TasksContext";
import { tasksReducer } from "./tasksReducer";
const initialTasks = [
{ id: 1, text: "Learn React", done: false },
{ id: 2, text: "Practice useReducer", done: true }
];
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
If your project uses React 18 or earlier, replace the two direct context elements with and .
Step 4: Create Custom Hooks
Custom Hooks make Context easier to consume and allow you to add clear error messages if a component is rendered outside the provider.
import { useContext } from "react";
import {
TasksContext,
TasksDispatchContext
} from "./TasksContext";
export function useTasks() {
const context = useContext(TasksContext);
if (context === null) {
throw new Error("useTasks must be used within TasksProvider");
}
return context;
}
export function useTasksDispatch() {
const context = useContext(TasksDispatchContext);
if (context === null) {
throw new Error(
"useTasksDispatch must be used within TasksProvider"
);
}
return context;
}
Step 5: Use the Provider in App.jsx
import { TasksProvider } from "./TasksProvider";
import AddTask from "./AddTask";
import TaskList from "./TaskList";
export default function App() {
return (
<TasksProvider>
<h1>My Task Manager</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}
Step 6: Dispatch an Action to Add a Task
import { useState } from "react";
import { useTasksDispatch } from "./TasksHooks";
let nextId = 3;
export default function AddTask() {
const [text, setText] = useState("");
const dispatch = useTasksDispatch();
function handleAddTask() {
const trimmedText = text.trim();
if (!trimmedText) return;
dispatch({
type: "added",
id: nextId++,
text: trimmedText
});
setText("");
}
return (
<div>
<input
value={text}
onChange={event => setText(event.target.value)}
placeholder="Enter a task"
/>
<button onClick={handleAddTask}>Add Task</button>
</div>
);
}
Production note: The module-level ID counter is suitable for illustrating the pattern in a simple demo, but it is not a robust ID strategy for larger applications. Consider using crypto.randomUUID() where supported or generating IDs in a dedicated data layer.
Step 7: Display and Update Tasks
import { useTasks, useTasksDispatch } from "./TasksHooks";
export default function TaskList() {
const tasks = useTasks();
const dispatch = useTasksDispatch();
function toggleTask(task) {
dispatch({
type: "changed",
task: { ...task, done: !task.done }
});
}
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => toggleTask(task)}
/>
<span>
{task.text}
</span>
</label>
<button
onClick={() => dispatch({
type: "deleted",
id: task.id
})}
>
Delete
</button>
</li>
))}
</ul>
);
}
This example demonstrates how multiple components can read or update the same task state without receiving the task list and action handlers through every intermediate component.
8. Reducer vs Context API
| Feature |
Reducer |
Context |
| Main purpose |
Centralizes state transition logic. |
Makes values available to descendants. |
| Primary API |
useReducer |
createContext and useContext |
| State updates |
Processes actions through a reducer. |
Does not manage state by itself. |
| Prop drilling |
Does not automatically solve it. |
Can reduce the need for intermediate props. |
| Best use |
Complex or related state transitions. |
Data shared by components at different depths. |
Important: Reducer and Context solve different problems. A reducer organizes updates; Context distributes values. Combining them can provide a lightweight state management pattern for many React applications.
9. useState vs useReducer vs Reducer and Context
| Approach |
Use It When |
Example |
useState |
State is simple and updates are straightforward. |
Input field, modal visibility. |
useReducer |
Several actions update related state. |
Form workflow, task list. |
| Reducer + Context |
Complex shared state is needed by distant components. |
Task manager, shopping cart, shared workflow. |
10. Advantages of Reducer and Context Patterns
- Centralized state logic: Related updates are handled in a reducer.
- Less prop drilling: Components can read shared data through Context.
- Better organization: Providers, reducers, and custom Hooks can be separated into files.
- Reusable logic: Multiple components can dispatch the same actions.
- Predictable updates: Explicit actions make state transitions easier to follow.
- Maintainability: The state model and its supported operations are easier to inspect.
- Testing: Pure reducer functions can be tested independently of the UI.
11. Limitations and Performance Considerations
- For very small components, using a reducer and multiple contexts may add unnecessary complexity.
- Components reading a Context can re-render when its provided value changes.
- Separating state and dispatch contexts can help avoid dispatch-only consumers subscribing to state changes through the same Context.
- Context does not automatically prevent re-renders caused by ordinary parent rendering.
- Large, frequently changing state may benefit from splitting Context by responsibility or using a dedicated state management library.
- Reducers must remain pure; handle asynchronous requests and other side effects in event handlers, Effects, or a suitable data layer.
- Use stable list keys and immutable updates to keep rendering predictable.
Performance Best Practices
- Keep each provider as close as practical to the components that need its data.
- Split unrelated state into separate contexts instead of putting everything into one large object.
- Keep reducer logic focused on state transitions.
- Use
useMemo or useCallback when profiling shows that unstable context values or callbacks are causing unnecessary work.
- Do not add memoization automatically without a demonstrated need.
12. Common Mistakes and Their Solutions
| Mistake |
Problem |
Solution |
| Mutating state directly |
Can lead to incorrect or unpredictable updates. |
Return new objects or arrays. |
| Missing an action type |
The reducer cannot identify the intended operation. |
Use explicit, consistent action types. |
| Forgetting to return state |
The reducer may return undefined. |
Return the next state for every supported action. |
| Using Context outside its provider |
The consumer receives the default value. |
Place the consumer inside the correct provider. |
| Passing the wrong provider value |
Consumers receive incorrect data. |
Check the provider's value prop. |
| Putting API calls inside a reducer |
Reducers stop being pure and predictable. |
Perform side effects outside the reducer. |
| Using one Context for unrelated state |
State dependencies become harder to maintain. |
Separate contexts by responsibility. |
| Overusing Context |
Simple local state becomes unnecessarily complex. |
Use local state when sharing is not needed. |
13. Practical Project: Task Management Application
Project Goal: Build a task manager using the Reducer and Context pattern.
Features
- Add new tasks.
- Mark tasks as completed or incomplete.
- Edit task descriptions.
- Delete tasks.
- Display all tasks in a list.
- Share task data between separate components without prop drilling.
Suggested Folder Structure
src/
App.jsx
TasksContext.jsx
tasksReducer.js
TasksProvider.jsx
TasksHooks.js
AddTask.jsx
TaskList.jsx
Project Workflow
- Create the initial task array.
- Write the reducer and define supported actions.
- Create state and dispatch contexts.
- Build a provider that calls
useReducer.
- Create custom Hooks for reading state and dispatching actions.
- Build the add-task and task-list components.
- Test every action and check that the UI updates correctly.
14. Interview Questions and Answers
Q1. What is a reducer in React?
A reducer is a pure function that receives the current state and an action and returns the next state.
Q2. What is the purpose of useReducer?
The useReducer Hook manages state using a reducer function and a dispatch function. It is useful when state updates involve multiple actions or related data.
Q3. What is Context in React?
Context lets a component make information available to descendant components without passing it through every intermediate component as props.
Q4. Why combine Reducer and Context?
A reducer centralizes state transitions, while Context makes the state and dispatch function available to components at different levels of the component tree.
Q5. What is dispatch?
dispatch sends an action to the reducer. React uses the reducer's returned state to update the UI.
Q6. What is an action?
An action is a value describing what happened or what update is requested. A common convention is an object with a type property and any necessary data.
Q7. Can a reducer call an API?
No. A reducer should be pure and must not perform side effects. API calls should be handled outside the reducer.
Q8. What is prop drilling?
Prop drilling is passing props through intermediate components that do not need the data themselves. Context can reduce this problem.
Q9. Does Context manage state by itself?
No. Context distributes a value. State can be managed using useState, useReducer, or another state management solution.
Q10. When should you avoid Reducer and Context?
Avoid this pattern when state is simple, local to one component, or does not need to be shared. A simple useState Hook may be enough.
Q11. Why should reducers avoid mutating state?
Immutable updates preserve the previous state and allow React to detect changes reliably. Return new arrays or objects when updating state.
Q12. What is a custom Hook in this pattern?
A custom Hook, such as useTasks(), wraps Context access and can centralize reusable logic and provider validation.
15. Practice Exercises
- Create a counter using
useReducer with increment, decrement, and reset actions.
- Create a Theme Context and let users switch between light and dark themes.
- Build a task reducer with add, edit, complete, and delete actions.
- Move task state and dispatch into separate Context objects.
- Create a provider component and custom Hooks for accessing state and dispatch.
- Build two components that read or update the same state without passing it through intermediate props.
- Write unit tests for the reducer's supported actions and unknown-action behavior.
16. Quick Revision
useReducer manages state through a reducer and dispatch function.
- A reducer calculates the next state from the current state and an action.
- Context shares values with descendant components.
createContext() creates a Context object.
useContext() reads the nearest matching provider value above a component.
- Reducer + Context combines centralized state updates with shared access.
- Separate state and dispatch contexts when that separation benefits the component structure.
- Reducers should be pure and should not mutate existing state.
- Use local state for simple component-specific data.
- Use shared state patterns only when the application's requirements justify them.
17. Official React Documentation
18. React JS Training Resources
Learn more about React components, Hooks, state management, Context API, and modern React development through the following resources:
Conclusion
The Reducer and Context pattern is a useful approach for managing complex shared state in React. The reducer keeps update logic organized, while Context provides shared access to state and dispatch functions. By combining these techniques with custom Hooks, immutable updates, and focused providers, developers can create React applications that are easier to maintain, understand, and test.